package edu.cmu.hcii.modalnodes;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import processing.core.PApplet;
import processing.core.PVector;
import SimpleOpenNI.SimpleOpenNI;
import edu.cmu.hcii.modalnodes.animation.Particle;
import edu.cmu.hcii.modalnodes.structure.Node;
import edu.cmu.hcii.modalnodes.structure.NodeGrid;

/**
 * The overall sequencer implementation; essentially the playing field for 
 * users.  Holds the nodes and tempo and relays input accordingly.
 * 
 * @author Chris
 *
 */
@SuppressWarnings("serial")
public class Sequencer extends PApplet {
	private static final Logger log = Logger.getLogger(Sequencer.class.getName());
	
	public static final int windowWidth = 900;
	public static final int windowHeight = 600;
	
	private static Tempo tempo;
	private static boolean paused;
	private static boolean debug;
	private static long pauseToggleTime;
	
	private static NodeGrid grid;
	
	public static float bpm;
	public static float nodeRadius;
	public static final long hoverThreshold = 1000;
	public static final long placeableThreshold = 1000;
	
	private Map<Integer, Node> focused;
	
	private Avatar avatar;
	public static float background = 0;
	public static float leftHand = (float) 0.95;
	
	int particleCount = 100;
	private Particle[] particlesRight = new Particle[particleCount/2];
	private Particle[] particlesLeft = new Particle[particleCount/2];
	
	
	//TODO fill this with the actual tones
	public static final String[] tones = new String[] {"lemo_beep.aiff", "level_1.aiff", 
		"lemo_beep.aiff", "level_1.aiff", "lemo_beep.aiff", "level_1.aiff", 
		"lemo_beep.aiff", "level_1.aiff", "lemo_beep.aiff", "level_1.aiff"};
	
	//First two digits = alpha
	public static final int[] colors = new int[] {0x33000000, 0x33FF0000, 0x3300FF00, 
		0x330000FF, 0x33FFFF00, 0x3300FFFF, 0x33FF00FF, 0x33C0C0C0, 0x33FFFFFF, 0x33FF6600, 0x339900CC};
	
	public SimpleOpenNI kinect;
	
	public static final int numUsers = 10;
	
	/**
	 * Make sure processing uses the right entry class.  We can add an 
	 * an additional string of "--presenter" at position 0 in the array
	 * to get a full screen mode.
	 * 
	 * @param args
	 */
	public static void main(String args[]) {
	    PApplet.main(new String[] { "edu.cmu.hcii.modalnodes.Sequencer" });
	}
	
	/**
	 * Setup the application using processing standards.  Set the application
	 * to 30 frames per second and make sure to use the OpenGL contexts (P2D 
	 * or P3D) to utilize the GPU.
	 */
	public void setup() {
		log.info("Setting up sequencer.");
		//See above, we may not need to specify p2d, but it is faster
		size(windowWidth, windowHeight, P2D);
		
		//TODO do something about this.  Setting the renderer calls setup again.
		kinect = new SimpleOpenNI(this, SimpleOpenNI.RUN_MODE_MULTI_THREADED);
		if (!kinect.enableDepth()) {
			log.severe("Cannot create depth map, exiting");
		} else {
			kinect.setMirror(true);
			kinect.enableUser(SimpleOpenNI.SKEL_PROFILE_ALL);
		}

		//Setup grid and node properties
		grid = new NodeGrid(this);
		nodeRadius = grid.getCellSize() / 2;
		
		//Roughly .5 bps (I think)
		bpm = windowWidth/200;
		tempo = new Tempo(this, bpm);
		
		//Crate new person, good for testing effects
		//disable with kinect
		avatar = new Avatar(this, windowWidth/2, windowHeight/2);
		
		paused = false;
		pauseToggleTime = 0;
		debug = false;
		focused = new HashMap<Integer, Node>();
		
		//Set up a mouse wheel listener
		//TODO This is just for the prototype.  Will remove once skeletons are used.
		addMouseWheelListener(new MouseWheelListener() { 
		    public void mouseWheelMoved(MouseWheelEvent mwe) { 
		      mouseWheel(mwe.getWheelRotation());
		}});
		
		Controls controls = new Controls(this); //create new console
		
		//create particles for each hand
		for (int i = 0; i < particleCount/2; i++) {
			particlesRight[i] = new Particle(this, 46,177,255); 
			particlesLeft[i] = new Particle(this, 255, 0, 0);
		}
	}
	
	/**
	 * The application's overall draw loop.
	 */
	public void draw() {
		pushStyle();
		
		background(3, 16, 59);
		fill(0, 30);
		noStroke();
		rect(0, 0, width, height); //draws transparent rectangle which gives a trail effect

		kinect.update();
		//image(kinect.depthImage(), 0, 0);
		
		for (int i = 0; i <= numUsers; i++) {
			if (i == 0) {
				if (focused.get(i) == null) {
					focused.put(i, grid.checkUserFocus(i, mouseX, mouseY));
				}
			} else if (kinect.isTrackingSkeleton(i)) {
				pushStyle();
				
				stroke(colors[i]);
				fill(colors[i]);
				
				PVector rHand = KinectUtilities.getProjectionJointPosition(kinect, i, SimpleOpenNI.SKEL_RIGHT_HAND);
				
				if (debug) KinectUtilities.drawSkeleton(this, kinect, i);
				//TODO 40 is a rather arbitrary value...
				ellipse(rHand.x, rHand.y, (525 / rHand.z) * 40, (525 / rHand.z) * 40);
				
				if (focused.get(i) == null) {
					focused.put(i, grid.checkUserFocus(i, (int)rHand.x, (int)rHand.y));
					
					if (KinectUtilities.handsTogether(kinect, i)) {
						createNode(i, rHand.x, rHand.y);
					}
					
					//Only allow T.O. if it was toggled more than 1 sec ago.
					if (KinectUtilities.timeOut(kinect, i) && 
						Calendar.getInstance().getTimeInMillis() - pauseToggleTime > 1000) {
						togglePause();
					}
				} else {
					//TODO: the size multiplier is Node.maxRad * 3
					focused.get(i).setRad((525 / rHand.z) * 150);
					focused.get(i).setPos(new PVector(rHand.x, rHand.y));
					
					//Allow the user to put down a node
					if (KinectUtilities.armRightAngle(kinect, i) && focused.get(i).isPlaceable()) placeNode(i);
				}
				
				popStyle();
			} else {
				grid.clearUserHover(i);
			}
		}

		grid.run();
		
		for (Node n : focused.values()) {
			if (n != null) n.run();
		}
		
		tempo.run();		
		popStyle();

		
		//draw the avatar disable for kinect
		avatar.display();

		fill(0, 0);
		noStroke(); 
		rect(0, 0, width, height);
		for (int i = 0; i < particleCount/2; i++) {
			particlesRight[i].update(avatar.yR, avatar.xR, avatar.yR);
			particlesRight[i].display(); 
			particlesRight[i].checkEdges();   
			particlesLeft[i].update(avatar.yL, avatar.xL, avatar.yL);
			particlesLeft[i].display(); 
			particlesLeft[i].checkEdges();    
		}
	}
	
	/**
	 * When a new user is detected, look for a start pose.
	 * 
	 * @param userId the id assigned to the new user
	 */
	public void onNewUser(int userId) {
	    log.info("New User Detected - " + userId);
	    
	    kinect.startPoseDetection("Psi", userId);
	}
	 
	/**
	 * Notify when a user has left the scene.
	 * 
	 * @param userId the id of the user who has left.
	 */
	public void onLostUser(int userId) 	{
		log.info("Lost user " + userId);
	}
	 
	/**
	 * When a user has begun a start pose, try to calibrate
	 * their skeleton.
	 * 
	 * @param pose the pose detected
	 * @param userId the id of the user doing the pose
	 */
	public void onStartPose(String pose, int userId) {
		log.info("User " + userId + " starting pose " + pose);
		
	    kinect.stopPoseDetection(userId);
	    kinect.requestCalibrationSkeleton(userId, true); 
	}
	 
	/**
	 * Notify that the calibration has started.
	 * 
	 * @param userId the id of the user being calibrated
	 */
	public void onStartCalibration(int userId) {
		log.info("Calibrating user " + userId);
	}
	 
	/**
	 * Report the calibration results.
	 * 
	 * @param userId the id of the user being calibrated
	 * @param successfull the result of the calibration
	 */
	public void onEndCalibration(int userId, boolean successfull) {
		if (successfull) { 
	    	log.info("Successfully calibrated user " + userId);
	    	
	        kinect.startTrackingSkeleton(userId); 
	    } else { 
	        log.info("Failed to calibrate user " + userId);
	        
	        kinect.startPoseDetection("Psi",userId);
	    }
	}
	
	/**
	 * When the mouse is clicked, create a focused node or, 
	 * if one already exists, add the focused to the node grid.
	 */
	public void mousePressed() {
		Node mouseNode = focused.get(0);
		
		if (mouseNode != null) {
			placeNode(0);
		} else {
			createNode(0, mouseX, mouseY);
		}
	}
	
	private void createNode(int userId, float x, float y) {
		Node newNode = new Node(this, new PVector(x, y, 0), nodeRadius);
		newNode.setFocused(true);
		focused.put(userId, newNode);
	}
	
	private void placeNode(int userId) {
		Node focusedNode = focused.get(userId);
		if (focusedNode != null) {
			grid.addNode(focusedNode);
			focusedNode.setFocused(false);
			focused.put(userId, null);
		}
	}
	
	/**
	 * Have the focused node locked onto the cursor until finalized.
	 */
	public void mouseMoved() {
		Node mouseNode = focused.get(0);
		
		if (mouseNode != null) mouseNode.setPos(new PVector(mouseX, mouseY));
	}
	
	/**
	 * Change the size of the focused node when the mouse wheel is moved.
	 * 
	 * @param delta the movement of the scroll wheel
	 */
	void mouseWheel(int delta) {
		Node mouseNode = focused.get(0); 
		
		if (mouseNode != null) mouseNode.setRad(mouseNode.getRad() + delta);
	}
	
	/**
	 * When p is pressed, pause the sequencer.  Use the arrow keys
	 * to modify the global tempo.
	 */
	public void keyPressed() {
		switch (key) {
			case 'p':
				togglePause();
				break;
			case 'd':
				debug = !debug;
				break;
			case CODED:
				switch (keyCode){
					case UP:
						log.info("Increase BPM");
						tempo.setBpm(tempo.getBpm() + 1);
						break;
					case DOWN:
						log.info("Descrease BPM");
						tempo.setBpm(tempo.getBpm() - 1);
						break;
					default:
						break;
				}
				break;
			default: 
				break;
		}
	}
	
	public static void togglePause() {
		log.info("Paused: " + !paused);
		pauseToggleTime = Calendar.getInstance().getTimeInMillis();
		paused = !paused;
	}

	/**
	 * @return the tempo
	 */
	public static Tempo getTempo() {
		return tempo;
	}

	/**
	 * @return the paused
	 */
	public static boolean isPaused() {
		return paused;
	}
	
	/**
	 * @return the debug
	 */
	public static boolean isDebug() {
		return debug;
	}
}
